/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.editor.model;

import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;

import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTML;

public class ListModel extends EditorModel {

    public ListModel(DocumentContext context) {
        super(context);
    }

    public boolean isList(Element e) {
        return isElementTag(e, HTML.Tag.OL)
                || isElementTag(e, HTML.Tag.UL);
    }

    public boolean hasList(Element e) {
        return getList(e) != null;
    }

    public Element getList(Element e) {
        for (int i = 0; i < e.getElementCount(); i++) {
            if (isList(e.getElement(i))) {
                return e.getElement(i);
            }
        }

        return null;
    }

    public boolean isList(int pos) {
        return getAncestor(pos, HTML.Tag.UL) != null || getAncestor(pos, HTML.Tag.OL) != null;
    }

    public boolean isListItem(int position) {
        return isElementTag(position, HTML.Tag.LI)
                || isParentElementTag(position, HTML.Tag.LI);
    }

    public boolean isFirstListItem(int position) {
        if (!isListItem(position)) {
            return false;
        }
        Element li = getAncestor(position, HTML.Tag.LI);

        return getChildIndex(li) == 0;
    }

    public boolean isLastListItem(int position) {
        if (!isListItem(position)) {
            return false;
        }
        Element li = getAncestor(position, HTML.Tag.LI);

        return getChildIndex(li) == li.getParentElement().getElementCount() - 1;
    }

    public boolean isEmptyListItem(int pos) throws BadLocationException {
        if (!isParentElementTag(pos, HTML.Tag.LI)) {
            return false;
        }

        Element li = getAncestor(pos, HTML.Tag.LI);
        return li.getEndOffset() - li.getStartOffset() <= 1;
    }

    public boolean isStartOfListItem(int pos) {
        if (!isParentElementTag(pos, HTML.Tag.LI)) {
            return false;
        }
        if (!isElementTag(getDocument().getParagraphElement(pos), HTML.Tag.IMPLIED)
                && !isElementTag(getDocument().getParagraphElement(pos), HTML.Tag.P)) {
            return false;
        }

        Element li = getAncestor(pos, HTML.Tag.LI);
        int start = li.getStartOffset();

        return pos == start;
    }

    public boolean isStartOfListItemParagraph(int pos) {
        Element e = getDocument().getParagraphElement(pos);
        return isElementTag(e, HTML.Tag.P) && pos == e.getStartOffset();
    }

    public void mergeListItemParagraph(int pos) throws BadLocationException,
            IOException {
        Element toMerge = getDocument().getParagraphElement(pos);
        if (!isElementTag(toMerge, HTML.Tag.P)) {
            return;
        }

        Element li = toMerge.getParentElement();
        StringBuffer buf = new StringBuffer("<li>");
        for (int i = 0; i < li.getElementCount(); i++) {
            Element e = li.getElement(i);

            if (e == toMerge) {
                String lastTag = lastTag(buf.toString());
                if (lastTag.equals(HTML.Tag.P.toString())) {
                    buf = new StringBuffer(removeLastTag(buf.toString()));
                }
                buf.append(getInnerHTML(toMerge));
                if (lastTag.equals(HTML.Tag.P.toString())) {
                    buf.append(lastTag);
                }
            } else {
                buf.append(getOuterHTML(e));
            }
        }
        buf.append("</li>");

        getDocument().setOuterHTML(li, buf.toString());
    }

    public StringBuffer mergeAdjacentLists(StringBuffer first, StringBuffer second) {
        String firstTag;
        while (lastTag(first.toString()).equals("/" + (firstTag = firstTag(second.toString())))
                && (firstTag.equals(HTML.Tag.UL.toString()) || firstTag.equals(HTML.Tag.OL.toString()))) {
            first.delete(first.lastIndexOf("<"), first.length());
            second.delete(0, second.indexOf(">") + 1);
        }

        return first.append(second);

    }

    public boolean isEndOfListItem(int pos) {
        if (!isParentElementTag(pos, HTML.Tag.LI)) {
            return false;
        }

        Element li = getAncestor(pos, HTML.Tag.LI);
        int end = li.getEndOffset() - 1;

        return pos == end;
    }

    public Element getList(int pos) {
        Element list = getAncestor(pos, HTML.Tag.OL);
        if (list == null) {
            list = getAncestor(pos, HTML.Tag.UL);
        }

        return list;
    }

    public void insertListItem(int pos) throws BadLocationException,
            IOException {
        Element li = getAncestor(pos, HTML.Tag.LI);
        if (isEndOfListItem(pos)) {
            getDocument().setOuterHTML(li, getOuterHTML(li) + "<li></li>");
            return;
        }

        splitElement(li, pos);
    }

    public void removeList(int pos) throws BadLocationException, IOException {
        Element li = getAncestor(pos, HTML.Tag.LI);
        remove(li.getParentElement());
    }

    public void removeListItem(int pos) throws BadLocationException,
            IOException {
        Element li = getAncestor(pos, HTML.Tag.LI);
        remove(li);
    }

    public void toggleListParagraphs(Range range, HTML.Tag tag) throws BadLocationException, IOException {
        Element paragraph = getRealParagraph(range.getStartOffset());
        if (paragraph.getName().equals(HTML.Tag.LI.toString()))
            paragraph = paragraph.getParentElement();
        Element container = getContainer(paragraph);

        boolean doParagraphToListItem = false;
        for (int i = 0; i < container.getElementCount(); i++) {
            Element e = container.getElement(i);

            if (e.getEndOffset() < range.getStartOffset()) {
                continue;
            }
            if (e.getStartOffset() > range.getEndOffset()) {
                break;
            }

            if (e.getName().equals(HTML.Tag.IMPLIED.toString()) || e.getName().equals(HTML.Tag.P.toString())) {
                doParagraphToListItem = true;
                break;
            }

            if (isList(e) && includesDifferentListTypes(e, tag)) {
                doParagraphToListItem = true;
                break;
            }

        }

        if (doParagraphToListItem) {
            paragraphsToListItems(range, tag);
        } else {
            listItemsToParagraphs(range);
        }
    }

    protected boolean includesDifferentListTypes(Element list, HTML.Tag tag) {
        if (!list.getName().equals(tag.toString())) {
            return true;
        }

        for (int i = 0; i < list.getElementCount(); i++) {
            Element e = list.getElement(i);

            if (isList(e)) {
                if (includesDifferentListTypes(e, tag)) {
                    return true;
                }
            }
        }

        return false;
    }

    public String setListType(Element list, HTML.Tag tag, Range range) throws BadLocationException, IOException {
        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < list.getElementCount(); i++) {
            Element e = list.getElement(i);

            if (e.getName().equals(HTML.Tag.LI.toString())) {
                StringBuffer listItem = new StringBuffer();
                String listType = list.getName();
                if (isWithinRange(e, range)) {
                    listType = tag.toString();
                }

                listItem.append("<" + listType + ">" + getOuterHTML(e) + "</" + listType + ">");
                buf = mergeAdjacentLists(buf, listItem);
            } else if (isList(e)) {
                String lastTag = lastTag(buf.toString());
                buf = new StringBuffer(removeLastTag(buf.toString()));
                StringBuffer l = new StringBuffer(setListType(e, tag, range));
                l.append("<" + lastTag + ">");
                buf = mergeAdjacentLists(buf, l);
            }
        }

        return buf.toString();
    }

    public String listItemsToParagraphs(Element list, Range range, int level) throws BadLocationException, IOException {
        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < list.getElementCount(); i++) {
            Element e = list.getElement(i);

            if (!isWithinRange(e, range)) {
                if (e.getName().equals(HTML.Tag.LI.toString())) {
                    StringBuffer li = new StringBuffer();
                    for (int l = 0; l < level; l++) {
                        li.append("<" + list.getName() + ">");
                    }
                    li.append(getOuterHTML(e));
                    for (int l = 0; l < level; l++) {
                        li.append("</" + list.getName() + ">");
                    }
                    buf = mergeAdjacentLists(buf, li);
                } else if (isList(e)) {
                    StringBuffer html = new StringBuffer(listItemsToParagraphs(e, range, level + 1));
                    buf = mergeAdjacentLists(buf, html);
                }
            } else {
                if (e.getName().equals(HTML.Tag.LI.toString())) {
                    for (int j = 0; j < e.getElementCount(); j++) {
                        buf.append("<p>" + getInnerHTML(e.getElement(j)) + "</p>");
                    }
                } else if (isList(e)) {
                    StringBuffer html = new StringBuffer(listItemsToParagraphs(e, range, level + 1));
                    buf = mergeAdjacentLists(buf, html);
                }
            }
        }

        return buf.toString();
    }

    public void paragraphsToListItems(Range range, HTML.Tag tag) throws BadLocationException, IOException {
        Element paragraph = getRealParagraph(range.getStartOffset());
        if (paragraph.getName().equals(HTML.Tag.LI.toString()))
            paragraph = paragraph.getParentElement();
        Element parent = getContainer(paragraph);

        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < parent.getElementCount(); i++) {
            Element e = parent.getElement(i);

            if (!isWithinRange(e, range)) {
                buf = mergeAdjacentLists(buf, new StringBuffer(getOuterHTML(e)));
            } else {
                StringBuffer html = new StringBuffer();

                if (isList(e)) {
                    html.append(setListType(e, tag, range));
                } else if (isParagraph(e)) {
                    html.append("<" + tag.toString() + ">" + "<li>" + getInnerHTML(e) + "</li>" + "</" + tag.toString() + ">");
                }

                buf = mergeAdjacentLists(buf, html);
            }
        }

        getDocument().setInnerHTML(parent, buf.toString());
    }
    
    public Element getListContainer(int pos) {
        Element e = context.getDocument().getParagraphElement(pos);
        
        while (e != null) {
            if (isList(e))
                break;
            
            e = e.getParentElement();
        }
        
        if (e != null)
            e = e.getParentElement();
        
        return e;
    }

    public void listItemsToParagraphs(Range range) throws BadLocationException, IOException {
        Element container = getListContainer(range.getStartOffset());

        for (int i = 0; i < container.getElementCount(); i++) {
            Element e = container.getElement(i);

            if (isWithinRange(e, range) && isList(e)) {
                Element parent = e.getParentElement();
                if (parent.getName().equals(HTML.Tag.LI.toString())) { // workaround for JDK glitch: substitute one level higher
                    setOuterHTML(parent, getStartTag(parent) + substituteElementByOuterHTMLGetParentInnerHTML(e, listItemsToParagraphs(e, range, 1)) + getEndTag(parent));
                } else
                    setOuterHTML(e, listItemsToParagraphs(e, range, 1));
            }
        }
    }

    protected void listItemToListParagraph(Element li)
            throws BadLocationException, IOException {
        Element previous = getPreviousSibling(li);
        while (previous != null
                && !previous.getName().equals(HTML.Tag.LI.toString())) {
            previous = getPreviousSibling(previous);
        }

        if (previous == null) { // there is no previous list item, so simply
            // convert the list item to a pargraph
            String text = getDocument().getText(li.getStartOffset(),
                    li.getEndOffset() - li.getStartOffset());
            getDocument().setOuterHTML(li, "<p>" + text + "</p>");
            return;
        }

        // else, add the current list item as a paragraph to the previous list
        // item
        StringBuffer buf = new StringBuffer("<li>" + getInnerHTML(previous));
        buf.append("<p>" + getInnerHTML(li.getElement(0)) + "</p>");
        for (int i = 1; i < li.getElementCount(); i++) {
            buf.append(getOuterHTML(li.getElement(i)));
        }
        buf.append("</li>");

        getDocument().setOuterHTML(previous, buf.toString());
        remove(li);
    }

    public void listItemToListParagraph(int pos) throws BadLocationException,
            IOException {
        // for now only first list item
        Element li = getAncestor(pos, HTML.Tag.LI);
        listItemToListParagraph(li);
    }

    public void indentRight(Range range) throws BadLocationException,
            IOException {
        Element firstLI = getAncestor(range.getStartOffset(), HTML.Tag.LI);
        Element lastLI = getAncestor(range.getEndOffset(), HTML.Tag.LI);
        if (firstLI.getParentElement() != lastLI.getParentElement()) // for now, do not implement indenting cross list borders
        {
            return;
        }

        Element list = firstLI.getParentElement();
        int first = getChildIndex(firstLI);
        if (first == 0) // for now, do not implement indenting that creates empty LI elements. It will likely not be desired
        {
            return;
        }

        int last = getChildIndex(lastLI);
        boolean listBefore = false;
        boolean listAfter = false;

        if (first > 0
                && hasList(list.getElement(first - 1))
                && getList(list.getElement(first - 1)).getName().equals(list.getName())) {
            listBefore = true; // merge with list before
        }
        StringBuffer indentListBuf = new StringBuffer("<" + list.getName() + ">");
        if (listBefore) {
            indentListBuf.append(getInnerHTML(getList(list.getElement(first - 1))));
        }

        for (int i = first; i <= last; i++) {
            Element li = list.getElement(i);
            indentListBuf.append(getOuterHTML(li));
        }

        indentListBuf.append("</" + list.getName() + ">");

        HashSet removes = new HashSet();
        for (int i = first; i <= last; i++) {
            removes.add(list.getElement(i));
        }

        Element insert = first > 0 ? list.getElement(first - 1) : list.getElement(first);

        StringBuffer listBuf = new StringBuffer();

        listBuf.append("<" + list.getName() + ">");

        for (int i = 0; i < list.getElementCount(); i++) {
            Element e = list.getElement(i);

            if (e == insert) {
                if (first == 0) // no list content, only child elements
                {
                    listBuf.append(getStartTag(insert) + indentListBuf.toString() + getEndTag(insert));
                } else if (listBefore) { // substitute list with element with extended list, leave other content unchanged
                    listBuf.append(getStartTag(insert));
                    for (int j = 0; j < insert.getElementCount(); j++) {
                        if (insert.getElement(j).getName().equals(list.getName())) {
                            listBuf.append(indentListBuf.toString());
                        } else {
                            listBuf.append(getOuterHTML(insert.getElement(j)));
                        }
                    }
                    listBuf.append(getEndTag(insert));
                } else // add new list as last child element
                {
                    listBuf.append(getStartTag(insert) + getInnerHTML(insert) + indentListBuf.toString() + getEndTag(insert));
                }

                continue;
            }

            if (removes.contains(e)) {
                continue;
            }

            listBuf.append(getOuterHTML(e));
        }

        listBuf.append("</" + list.getName() + ">");

        getDocument().setOuterHTML(list, listBuf.toString());
    }

    public void indentLeft(Range range) throws BadLocationException,
            IOException {
        Element firstLI = getAncestor(range.getStartOffset(), HTML.Tag.LI);
        Element lastLI = getAncestor(range.getEndOffset(), HTML.Tag.LI);
        if (firstLI.getParentElement() != lastLI.getParentElement()) // for now, do not implement indenting cross list borders
        {
            return;
        }

        Element list = firstLI.getParentElement();
        boolean listWithinList = list.getParentElement().getParentElement().getName().equals(
                HTML.Tag.UL.toString())
                || list.getParentElement().getParentElement().getName().equals(
                HTML.Tag.OL.toString());

        if (listWithinList) {
            indentLeftWithinList(firstLI, lastLI);
            return;
        }

        // list is top level list => indented LIs become paragraphs
        int first = getChildIndex(firstLI);
        int last = getChildIndex(lastLI);

        StringBuffer buf = new StringBuffer();
        // substitute the current list by up to three elements:

        // 1. a list built of the current list until the first selected element
        if (first != 0) {
            buf.append("<" + list.getName() + ">");
        }
        for (int i = 0; i < first; i++) {
            buf.append(getOuterHTML(list.getElement(i)));
        }
        if (first != 0) {
            buf.append("</" + list.getName() + ">");
        }

        // 2. the selected list items and
        for (int i = first; i <= last; i++) {
            Element e = list.getElement(i);

            if (e.getName().equals(HTML.Tag.LI.toString())) // convert list items to paragraphs if no outer list exists
            {
                buf.append("<p>" + getInnerHTML(e) + "</p>");
            } else {
                buf.append(getOuterHTML(e));
            }
        }

        // 3. a list built of the current list starting from the list item after
        // the last selected list item */
        if (last + 1 < list.getElementCount()) {
            buf.append("<" + list.getName() + ">");
        }
        for (int i = last + 1; i < list.getElementCount(); i++) {
            buf.append(getOuterHTML(list.getElement(i)));
        }
        if (last + 1 < list.getElementCount()) {
            buf.append("</" + list.getName() + ">");
        }

        if (listWithinList) {
            setOuterHTML(list, buf.toString());
        } else {
            getDocument().setOuterHTML(list, buf.toString());
        }
    }

    /**
     * Indent left some contiguous elements of the current list, which is contained in another list
     * of the same type.
     *
     * @param firstLI The first list element to be indented
     * @param lastLi The last list element to be indented
     */
    protected void indentLeftWithinList(Element firstLI, Element lastLI) throws BadLocationException, IOException {
        Element list = firstLI.getParentElement();
        Element parentLI = list.getParentElement();
        int first = getChildIndex(firstLI);
        int last = getChildIndex(lastLI);

        // analyse the list
        boolean hasContent = false;
        for (int i = 0; i < parentLI.getElementCount(); i++) {
            if (!parentLI.getElement(i).getName().equals(HTML.Tag.LI.toString())) {
                hasContent = true;
            }
        }

        boolean parentLIBecomesEmpty = false;
        if (!hasContent && first == 0 && last == list.getElementCount() - 1) {
            parentLIBecomesEmpty = true;
        }

        StringBuilder buf = new StringBuilder();

        // 1. the reduced parent LI
        if (!parentLIBecomesEmpty) {
            buf.append(getStartTag(parentLI));
            for (int j = 0; j < parentLI.getElementCount(); j++) {
                if (parentLI.getElement(j) != list) {
                    buf.append(getOuterHTML(parentLI.getElement(j)));
                } else {
                    // add list before firstLI
                    if (first > 0) {
                        buf.append(getStartTag(list));
                        for (int k = 0; k < list.getElementCount(); k++) {
                            if (k < first) {
                                buf.append(getOuterHTML(list.getElement(k)));
                            }
                        }
                        buf.append(getEndTag(list));
                    }
                }
            }
            buf.append(getEndTag(parentLI));
        }

        // 2. LI elements created after the parent LI
        if (first != last) // else only indented element will be done in 3.
        {
            for (int j = first; j < last; j++) {
                buf.append(getOuterHTML(list.getElement(j)));
            }
        }

        // 3. last LI takes the following list elements
        buf.append(getStartTag(lastLI));
        buf.append(getInnerHTML(lastLI));
        if (last != list.getElementCount() - 1) {
            buf.append("<" + list.getName() + ">");
            for (int k = last + 1; k < list.getElementCount(); k++) {
                buf.append(getOuterHTML(list.getElement(k)));
            }
            buf.append("</" + list.getName() + ">");
        }
        buf.append(getEndTag(lastLI));

        getDocument().setOuterHTML(parentLI, buf.toString());
    }

    public void mergeParagraphWithList(int pos) throws BadLocationException, IOException {
        Element p = getDocument().getParagraphElement(pos);
        Element pli = getDocument().getParagraphElement(pos - 1);
        getDocument().setInnerHTML(pli, getInnerHTML(pli) + getInnerHTML(p));
        remove(p);
    }
}
